<?php
/**
 * Copyright (c) 2018-2022.
 * This file is part of the moonpie production
 *   (c) johnzhang <875010341@qq.com>
 *   This source file is subject to the MIT license that is bundled
 *  with this source code in the file LICENSE.
 */

namespace Moonpie\Macro\HuaweiCloud\Kernel;


use EasyWeChat\Kernel\Contracts\AccessTokenInterface;
use Psr\Http\Message\RequestInterface;

class SkToken implements AccessTokenInterface
{
    const DATE_FORMAT = "Ymd\THis\Z";
    const KEY_ALGORITHM      = 'SDK-HMAC-SHA256';
    const HEADER_X_DATE         = 'X-Sdk-Date';
    const HEADER_HOST           = 'host';
    const HEADER_AUTHORIZATION  = 'Authorization';
    const HEADER_CONTENT_SHA256 = 'X-Sdk-Content-Sha256';
    private $accessKeyId;
    private $secretKey;
    protected $reqTime = null;
    protected $signedHeaders = [];

    public function __construct($accessKeyId, $secretKey)
    {
        $this->accessKeyId = $accessKeyId;
        $this->secretKey   = $secretKey;
        $this->prepareReqTime();
    }
    protected function prepareReqTime()
    {
        if (is_null($this->reqTime)) {
            $this->reqTime = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
        }
        return $this->reqTime;
    }

    public function getToken(): array
    {
        return [
            'sk' => $this->secretKey,
            'ak' => $this->accessKeyId,
        ];
    }

    public function refresh(): AccessTokenInterface
    {
        return new static($this->accessKeyId, $this->secretKey);
    }

    public function applyToRequest(
        RequestInterface $request,
        array $requestOptions = []
    ): RequestInterface {
        $t = $this->prepareReqTime();
        if (!$request->hasHeader(static::HEADER_X_DATE)) {
            $request = $request->withHeader(static::HEADER_X_DATE, $t->format(static::DATE_FORMAT));
        }

        $r = $request->withHeader(static::HEADER_AUTHORIZATION, $this->signRequest($request))
            ->withUri($request->getUri()->withQuery($this->canonicalQueryString($request)))
        ;

        return $r;
    }

    protected function queryEscape($string)
    {
        $entities     = array('+', "%7E");
        $replacements = array('%20', "~");

        return str_replace($entities, $replacements, urlencode($string));
    }

    protected function findHeader(RequestInterface $request, $header)
    {
        if ($request->hasHeader($header)) {
            return $request->getHeader($header);
        }

        return null;
    }

    /*
     *
     * Build a CanonicalRequest from a regular request string
    //
    // CanonicalRequest =
    //  HTTPRequestMethod + '\n' +
    //  CanonicalURI + '\n' +
    //  CanonicalQueryString + '\n' +
    //  CanonicalHeaders + '\n' +
    //  SignedHeaders + '\n' +
    //  HexEncode(Hash(RequestPayload))
    */
    protected function canonicalRequest(RequestInterface $r)
    {
        $CanonicalURI         = $this->canonicalURI($r);
        $CanonicalQueryString = $this->canonicalQueryString($r);
        $canonicalHeaders     = $this->canonicalHeaders($r);
        $hash                 = $this->findHeader(
            $r,
            static::HEADER_CONTENT_SHA256
        );
        if (!$hash) {
            $hash = hash("sha256", $r->getBody()->__toString());
        }
        $signedHeaders = implode(';', $this->signedHeaders);
        $ret = [
            $r->getMethod(),
            $CanonicalURI,
            $CanonicalQueryString,
            $canonicalHeaders,
            $signedHeaders,
            $hash,
        ];
        //var_dump($ret);exit;

        return implode(
            "\n",
            $ret
        );
    }

    public function canonicalURI(RequestInterface $request)
    {
        $uri  = $request->getUri();
        $path = $uri->getPath();

        $pattens = explode("/", $path);
        $uris    = [];
        foreach ($pattens as $v) {
            array_push($uris, $this->queryEscape($v));
        }
        $url_path = join("/", $uris);

        return rtrim($url_path, '/').'/';
    }

    public function canonicalQueryString(RequestInterface $request)
    {
        $uri    = $request->getUri();
        $q      = $uri->getQuery();
        parse_str($q, $params);

        $keys = [];
        foreach ($params as $key => $value) {
            array_push($keys, $key);
        }
        sort($keys);
        $a = array();
        foreach ($keys as $key) {
            $k     = $this->queryEscape($key);
            $value = $params[$key];
            if (is_array($value)) {
                sort($value);
                foreach ($value as $v) {
                    $kv = "$k=".$this->queryEscape($v);
                    array_push($a, $kv);
                }
            } else {
                $kv = "$k=".$this->queryEscape($value);
                array_push($a, $kv);
            }
        }

        return join("&", $a);
    }

    public function canonicalHeaders(RequestInterface $request)
    {
        $headers = $request->getHeaders();
        $this->signedHeaders = [];
        $hs = [];
        foreach($headers as $k => $header) {
            foreach((array) $header as $h) {
                $hs[strtolower($k)] = trim($h);
                $this->signedHeaders[] = strtolower($k);
            }
        }
        sort($this->signedHeaders);
        $return = [];
            foreach($this->signedHeaders as $h) {
                array_push($return, sprintf("%s:%s", $h, $hs[$h]));
            }
        return implode("\n", $return) . "\n";
    }
    public function signRequest(RequestInterface $request)
    {
        $sign_string = hash_hmac('sha256', $this->signString($request), $this->secretKey);
        return strtr(
            '{algorithm} Access={ak}, SignedHeaders={sign_headers}, Signature={signature}',
            [
                '{algorithm}' => static::KEY_ALGORITHM,
                '{ak}' => $this->accessKeyId, '{sign_headers}' => implode(';', $this->signedHeaders),
                '{signature}' => $sign_string,
            ]
        );
    }
    protected function signString(RequestInterface $request)
    {
        $r = $this->canonicalRequest($request);
        $keys = [
            static::KEY_ALGORITHM,
            $this->prepareReqTime()->format(static::DATE_FORMAT),
            hash('sha256', $r),
        ];
        return implode("\n", $keys);
    }
}